<?php
/**
 * Yadis service manager to be used during yadis-driven authentication
 * attempts.
 *
 * @package OpenID
 */

/**
 * The base session class used by the Auth_Yadis_Manager.  This
 * class wraps the default PHP session machinery and should be
 * subclassed if your application doesn't use PHP sessioning.
 *
 * @package OpenID
 */
class Auth_Yadis_PHPSession {

  /**
   * Set a session key/value pair.
   *
   * @param string $name The name of the session key to add.
   * @param string $value The value to add to the session.
   */
  function set($name, $value) {
    $_SESSION[$name] = $value;
  }

  /**
   * Get a key's value from the session.
   *
   * @param string $name The name of the key to retrieve.
   * @param string $default The optional value to return if the key
   * is not found in the session.
   * @return string $result The key's value in the session or
   * $default if it isn't found.
   */
  function get($name, $default = null) {
    if (array_key_exists($name, $_SESSION)) {
      return $_SESSION[$name];
    } else {
      return $default;
    }
  }

  /**
   * Remove a key/value pair from the session.
   *
   * @param string $name The name of the key to remove.
   */
  function del($name) {
    unset($_SESSION[$name]);
  }

  /**
   * Return the contents of the session in array form.
   */
  function contents() {
    return $_SESSION;
  }
}

/**
 * A session helper class designed to translate between arrays and
 * objects.  Note that the class used must have a constructor that
 * takes no parameters.  This is not a general solution, but it works
 * for dumb objects that just need to have attributes set.  The idea
 * is that you'll subclass this and override $this->check($data) ->
 * bool to implement your own session data validation.
 *
 * @package OpenID
 */
class Auth_Yadis_SessionLoader {

  /**
   * Override this.
   *
   * @access private
   */
  function check($data) {
    return true;
  }

  /**
   * Given a session data value (an array), this creates an object
   * (returned by $this->newObject()) whose attributes and values
   * are those in $data.  Returns null if $data lacks keys found in
   * $this->requiredKeys().  Returns null if $this->check($data)
   * evaluates to false.  Returns null if $this->newObject()
   * evaluates to false.
   *
   * @access private
   */
  function fromSession($data) {
    if (! $data) {
      return null;
    }
    
    $required = $this->requiredKeys();
    
    foreach ($required as $k) {
      if (! array_key_exists($k, $data)) {
        return null;
      }
    }
    
    if (! $this->check($data)) {
      return null;
    }
    
    $data = array_merge($data, $this->prepareForLoad($data));
    $obj = $this->newObject($data);
    
    if (! $obj) {
      return null;
    }
    
    foreach ($required as $k) {
      $obj->$k = $data[$k];
    }
    
    return $obj;
  }

  /**
   * Prepares the data array by making any necessary changes.
   * Returns an array whose keys and values will be used to update
   * the original data array before calling $this->newObject($data).
   *
   * @access private
   */
  function prepareForLoad($data) {
    return array();
  }

  /**
   * Returns a new instance of this loader's class, using the
   * session data to construct it if necessary.  The object need
   * only be created; $this->fromSession() will take care of setting
   * the object's attributes.
   *
   * @access private
   */
  function newObject($data) {
    return null;
  }

  /**
   * Returns an array of keys and values built from the attributes
   * of $obj.  If $this->prepareForSave($obj) returns an array, its keys
   * and values are used to update the $data array of attributes
   * from $obj.
   *
   * @access private
   */
  function toSession($obj) {
    $data = array();
    foreach ($obj as $k => $v) {
      $data[$k] = $v;
    }
    
    $extra = $this->prepareForSave($obj);
    
    if ($extra && is_array($extra)) {
      foreach ($extra as $k => $v) {
        $data[$k] = $v;
      }
    }
    
    return $data;
  }

  /**
   * Override this.
   *
   * @access private
   */
  function prepareForSave($obj) {
    return array();
  }
}

/**
 * A concrete loader implementation for Auth_OpenID_ServiceEndpoints.
 *
 * @package OpenID
 */
class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader {

  function newObject($data) {
    return new Auth_OpenID_ServiceEndpoint();
  }

  function requiredKeys() {
    $obj = new Auth_OpenID_ServiceEndpoint();
    $data = array();
    foreach ($obj as $k => $v) {
      $data[] = $k;
    }
    return $data;
  }

  function check($data) {
    return is_array($data['type_uris']);
  }
}

/**
 * A concrete loader implementation for Auth_Yadis_Managers.
 *
 * @package OpenID
 */
class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader {

  function requiredKeys() {
    return array('starting_url', 'yadis_url', 'services', 'session_key', '_current', 'stale');
  }

  function newObject($data) {
    return new Auth_Yadis_Manager($data['starting_url'], $data['yadis_url'], $data['services'], $data['session_key']);
  }

  function check($data) {
    return is_array($data['services']);
  }

  function prepareForLoad($data) {
    $loader = new Auth_OpenID_ServiceEndpointLoader();
    $services = array();
    foreach ($data['services'] as $s) {
      $services[] = $loader->fromSession($s);
    }
    return array('services' => $services);
  }

  function prepareForSave($obj) {
    $loader = new Auth_OpenID_ServiceEndpointLoader();
    $services = array();
    foreach ($obj->services as $s) {
      $services[] = $loader->toSession($s);
    }
    return array('services' => $services);
  }
}

/**
 * The Yadis service manager which stores state in a session and
 * iterates over <Service> elements in a Yadis XRDS document and lets
 * a caller attempt to use each one.  This is used by the Yadis
 * library internally.
 *
 * @package OpenID
 */
class Auth_Yadis_Manager {

  /**
   * Intialize a new yadis service manager.
   *
   * @access private
   */
  function Auth_Yadis_Manager($starting_url, $yadis_url, $services, $session_key) {
    // The URL that was used to initiate the Yadis protocol
    $this->starting_url = $starting_url;
    
    // The URL after following redirects (the identifier)
    $this->yadis_url = $yadis_url;
    
    // List of service elements
    $this->services = $services;
    
    $this->session_key = $session_key;
    
    // Reference to the current service object
    $this->_current = null;
    
    // Stale flag for cleanup if PHP lib has trouble.
    $this->stale = false;
  }

  /**
   * @access private
   */
  function length() {
    // How many untried services remain?
    return count($this->services);
  }

  /**
   * Return the next service
   *
   * $this->current() will continue to return that service until the
   * next call to this method.
   */
  function nextService() {
    
    if ($this->services) {
      $this->_current = array_shift($this->services);
    } else {
      $this->_current = null;
    }
    
    return $this->_current;
  }

  /**
   * @access private
   */
  function current() {
    // Return the current service.
    // Returns None if there are no services left.
    return $this->_current;
  }

  /**
   * @access private
   */
  function forURL($url) {
    return in_array($url, array($this->starting_url, $this->yadis_url));
  }

  /**
   * @access private
   */
  function started() {
    // Has the first service been returned?
    return $this->_current !== null;
  }
}

/**
 * State management for discovery.
 *
 * High-level usage pattern is to call .getNextService(discover) in
 * order to find the next available service for this user for this
 * session. Once a request completes, call .cleanup() to clean up the
 * session state.
 *
 * @package OpenID
 */
class Auth_Yadis_Discovery {
  
  /**
   * @access private
   */
  var $DEFAULT_SUFFIX = 'auth';
  
  /**
   * @access private
   */
  var $PREFIX = '_yadis_services_';

  /**
   * Initialize a discovery object.
   *
   * @param Auth_Yadis_PHPSession $session An object which
   * implements the Auth_Yadis_PHPSession API.
   * @param string $url The URL on which to attempt discovery.
   * @param string $session_key_suffix The optional session key
   * suffix override.
   */
  function Auth_Yadis_Discovery(&$session, $url, $session_key_suffix = null) {
    /// Initialize a discovery object
    $this->session = & $session;
    $this->url = $url;
    if ($session_key_suffix === null) {
      $session_key_suffix = $this->DEFAULT_SUFFIX;
    }
    
    $this->session_key_suffix = $session_key_suffix;
    $this->session_key = $this->PREFIX . $this->session_key_suffix;
  }

  /**
   * Return the next authentication service for the pair of
   * user_input and session. This function handles fallback.
   */
  function getNextService($discover_cb, &$fetcher) {
    $manager = $this->getManager();
    if (! $manager || (! $manager->services)) {
      $this->destroyManager();
      
      list($yadis_url, $services) = call_user_func($discover_cb, $this->url, $fetcher);
      
      $manager = $this->createManager($services, $yadis_url);
    }
    
    if ($manager) {
      $loader = new Auth_Yadis_ManagerLoader();
      $service = $manager->nextService();
      $this->session->set($this->session_key, serialize($loader->toSession($manager)));
    } else {
      $service = null;
    }
    
    return $service;
  }

  /**
   * Clean up Yadis-related services in the session and return the
   * most-recently-attempted service from the manager, if one
   * exists.
   *
   * @param $force True if the manager should be deleted regardless
   * of whether it's a manager for $this->url.
   */
  function cleanup($force = false) {
    $manager = $this->getManager($force);
    if ($manager) {
      $service = $manager->current();
      $this->destroyManager($force);
    } else {
      $service = null;
    }
    
    return $service;
  }

  /**
   * @access private
   */
  function getSessionKey() {
    // Get the session key for this starting URL and suffix
    return $this->PREFIX . $this->session_key_suffix;
  }

  /**
   * @access private
   *
   * @param $force True if the manager should be returned regardless
   * of whether it's a manager for $this->url.
   */
  function &getManager($force = false) {
    // Extract the YadisServiceManager for this object's URL and
    // suffix from the session.
    

    $manager_str = $this->session->get($this->getSessionKey());
    $manager = null;
    
    if ($manager_str !== null) {
      $loader = new Auth_Yadis_ManagerLoader();
      $manager = $loader->fromSession(unserialize($manager_str));
    }
    
    if ($manager && ($manager->forURL($this->url) || $force)) {
      return $manager;
    } else {
      $unused = null;
      return $unused;
    }
  }

  /**
   * @access private
   */
  function &createManager($services, $yadis_url = null) {
    $key = $this->getSessionKey();
    if ($this->getManager()) {
      return $this->getManager();
    }
    
    if ($services) {
      $loader = new Auth_Yadis_ManagerLoader();
      $manager = new Auth_Yadis_Manager($this->url, $yadis_url, $services, $key);
      $this->session->set($this->session_key, serialize($loader->toSession($manager)));
      return $manager;
    } else {
      // Oh, PHP.
      $unused = null;
      return $unused;
    }
  }

  /**
   * @access private
   *
   * @param $force True if the manager should be deleted regardless
   * of whether it's a manager for $this->url.
   */
  function destroyManager($force = false) {
    if ($this->getManager($force) !== null) {
      $key = $this->getSessionKey();
      $this->session->del($key);
    }
  }
}

?>